TypeSchema/TS: make Bundle generic with IR-stored generic params#148
Open
TypeSchema/TS: make Bundle generic with IR-stored generic params#148
Conversation
3868aac to
a9cee6f
Compare
Parent interfaces now become generic when any of their fields reference
a generic nested type, and the field reference carries the type
parameter through.
Before: `Bundle { entry?: BundleEntry[] }` (always BundleEntry<Resource>)
After: `Bundle<T extends Resource = Resource> { entry?: BundleEntry<T>[] }`
Default `= Resource` keeps existing call sites working. Callers can now
narrow, e.g. `Bundle<Patient | Observation>`.
Refactored the generic-params computation in generateType() into a
reusable helper (computeGenericInfo) and threaded per-nested-type info
from generateNestedTypes() back into the parent generation.
- Bundle.ts now declares `Bundle<T extends Resource = Resource>` with `entry?: BundleEntry<T>[]`. - Added demo tests showing discriminated-union narrowing via `Bundle<Patient | Observation>` and backwards-compat default.
Drop the standalone computeGenericInfo helper and the GenericInfo type in favor of inlining the (small) logic directly into generateType. The helper duplicated the existing typeFamilyFields detection that already worked; the genuinely new bit is just the nested-type pass-through. generateType now returns the parent's GenericParam[] (only paramList is needed by callers — fieldMap and nestedArgsByField are local concerns), and generateNestedTypes threads a Record<string, GenericParam[]> back to the parent. Net change vs main: +61/-32 (was +81/-36). No behavior change.
Collapse the two-list collection (typeFamilyFields + nestedFields) into a single discriminated Contribution[] driven by tsIndex.resolveType, which handles nested and non-nested identifiers uniformly. - introduce: field type resolves to a typeFamily root → bind a fresh param - passthrough: field type resolves to a generic nested type → inherit its params Extract the collection into a free helper (collectGenericContributions) to keep generateType under the cognitive-complexity cap. Render pass becomes one loop over contributions; naming policy (T vs TFieldName) keys off the contribution shape.
Add `generic.params` to `NestedTypeSchema` (with `GenericParam = { name, constraint: TypeIdentifier }`).
Populate during `mkTypeSchemaIndex` after `populateTypeFamily` — for each nested in URL-sorted order,
collect contributions: introduce (field type is a typeFamily root) or passthrough (field type is a
generic-bearing nested with already-populated `generic.params`).
The TS writer reads `target.generic?.params` directly instead of threading a per-pass
`nestedGenericParams` accumulator. `generateType` and `generateNestedTypes` lose their threading
parameter and `generateType` no longer needs a return value.
Behavior change: this fixes an order-dependency in the old writer where a nested type couldn't see
its sibling nesteds' generic params (because they hadn't been generated yet during the sequential
pass). With the IR populated up front, the writer sees the full picture. As a result, e.g.
`BundleEntry` now passes through `BundleEntryResponse`'s generic, becoming
`BundleEntry<TResource extends Resource = Resource, T extends Resource = Resource>` with
`resource?: TResource; response?: BundleEntryResponse<T>;`. `Bundle<T>` itself stays one-param.
Add `sourceField` to `GenericParam` IR type — the field that originally introduced
the param (preserved through passthrough). Naming policy:
- single param → "T" (short)
- multiple params → `T${UpperFirst(sourceField)}` per param
So a param introduced deep in `BundleEntryResponse.outcome` surfaces in `BundleEntry`
as `TOutcome` rather than the local `T`, regardless of how many carrier hops it took
to get there.
Generator-side change: dedup raw params by `sourceField` (not by name). Writer mirrors
the populator's logic for top-level schemas.
Populator now iterates to a fixpoint instead of one URL-sorted pass — passthrough
between siblings means earlier-processed nesteds don't see later siblings'
introduces. Without fixpoint, BundleEntry's IR-stored `generic.params` would be
stale (one param, missing the response passthrough), causing Bundle (top-level) to
emit wrong arity.
Effect on emit:
Before: BundleEntry<TResource, T> with response: BundleEntryResponse<T>
After: BundleEntry<TResource, TOutcome> with response: BundleEntryResponse<TOutcome>
Bundle now exposes both: Bundle<TResource, TOutcome> with entry: BundleEntry<TResource, TOutcome>[]
Use positional names (T1, T2, …) for multi-param schemas instead of the sourceField-derived TResource/TOutcome. Single-param schemas still emit "T". The IR-stored sourceField is preserved (still used to align passthrough args across nesting hops), only the rendered name changes. Effect on emit: Before: BundleEntry<TResource, TOutcome>; entry: BundleEntry<TResource, TOutcome>[] After: BundleEntry<T1, T2>; entry: BundleEntry<T1, T2>[]
dd1acc4 to
69a111f
Compare
- Define `GenericInfo = { params: GenericParam[] }` once and use it on both
`SpecializationTypeSchemaBody` (Resource, ComplexType, Logical) and
`NestedTypeSchema`. Symmetric IR surface — any generic-bearing schema carries
its params, not just nested ones.
- Populator (`populateGeneric`, renamed from `populateNestedGeneric`) iterates
over a single carrier list of top-level specializations + their nested in
fixpoint. Profiles are intentionally out of scope — only their nested types
participate.
- `collectGenericContributions` now treats top-level schemas symmetrically with
nested: a field whose target carries `generic.params` becomes a passthrough
regardless of whether the target is top-level or nested.
- TS writer's `generateType` reads `schema.generic?.params` directly (no local
recomputation of param names). Per-field substitution maps still come from
the contributions list, aligned to schema params by `sourceField`.
Hardcoded TS specials (`Reference<T extends string>`, `Coding<T extends string>`,
`CodeableConcept<T extends string>`) remain in the writer — their constraint is
a primitive (`string`) and the rendering (template-literal Reference type, enum
narrowing per field) is TS-flavored, not language-neutral IR concerns.
Effect on emit: schemas like `DomainResource` and CDA's `ClinicalDocument` now
also carry their populator-computed generics through to the writer (rather
than being recomputed locally), making the typeFamily-based generic story
fully data-driven from the IR.
`path` is now `string[]` carrying the full origin trail from the schema down to the typeFamily-rooted field that introduces the param. Single-segment for direct introduce (e.g. `["outcome"]`), grown by passthrough as the param surfaces through carrier fields (e.g. `["response", "outcome"]`, then `["entry", "response", "outcome"]`). Dedup is by leaf segment of the path — multiple fields with the same deepest origin still share one generic param (so callers narrow once to update many fields). Param identity stays the same as before; only the IR data is richer. Naming policy unchanged: single param → "T", multiple → positional T1/T2/…. Writer alignment also matches by leaf segment of `path` (with fallback to the child param's typeVar) so passthrough args render correctly across nesting hops.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #145.
Summary
Originally scoped to make
Bundle<T>generic; expanded into an IR-level refactor so that any specialization schema (top-level or nested) carrying type-family-rooted or generic-bearing fields gets generic params populated on the IR (schema.generic.params), and language writers render directly from that. Profiles intentionally out of scope.Generated output (before / after)
Before:
After:
Defaults preserve every existing call site. Callers can narrow via either or both params.
Usage
Bundle<Patient | Observation>works becauseT2defaults toResource.TypeSchema IR change
sourceFieldrecords the deep field that originally introduced the param (the typeFamily-rooted leaf), so the same conceptual param keeps its identity across passthrough hops and parents can align passthrough args even after multi-level inheritance.Population happens in
mkTypeSchemaIndexafterpopulateTypeFamily, iterating to a fixpoint over all specialization schemas (top-level + nested) so order doesn't matter.Naming policy
TT1,T2, … positional namesPositional avoids name churn when the underlying field set changes;
sourceFieldon each param keeps deep-origin info available for tooling and for aligning passthrough args.Writer changes
generateTypereadsschema.generic?.paramsdirectly (no local recomputation).fieldMap,nestedArgsByField) come from the contributions list, aligned to schema params bysourceField.Reference<T extends string>,Coding<T extends string>,CodeableConcept<T extends string>) stay in the writer — their constraint isstring, not a typeFamily root, and the rendering (template-literalReference.reference, per-field enum narrowing) is TS-flavored.Tests
typescript.test.ts— assertions and snapshots forBundleEntry<T1, T2>,Bundle<T1, T2>,BundleEntryResponse<T>propagation pathsintrospection.test.ts— snapshots include the newgeneric.paramsIR field on top-level and nested schemascda.test.ts—ClinicalDocumentnow also carries propagated generic params (via fields likeAuthor,Component, etc.)resource.test.ts— demo usingBundle<Patient | Observation>and TS-5.5 type-predicate inference